package com.hsc.mvc;

import com.hsc.mvc.bind.ParameterBindStrategy;
import com.hsc.mvc.handler.ResponseHandleStrategy;
import com.hsc.mvc.tag.AopMappingTag;
import com.hsc.mvc.tag.MappingTag;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @author
 * @date 2025-05-25
 * @Description 装载执行过程中涉及的切面对象和目标对象，并执行
 */
public class ExecutorProxy {

    /**
     * 里面装载者从配置文件或者注解配置的切面信息
     * 每次请求  会经过多个符合要求的拦截器 但是并不是所有的拦截器都会经过
     */
    private List<AopMappingTag> interceptors;

    private Configuration configuration;

    // //缓存所有字符串请求参数 （普通请求的字符串和文件上传请求的字符串）
    private Map<String, String[]> parameterMap = new HashMap<>();
    private Map<String, MvcFile[]> fileParamValues = new HashMap<>();


    // 我们将request 对象和respond 保存在ExecutorProxy 进行使用
    private HttpServletRequest request;
    private HttpServletResponse response;

    // 设置request 对象
    public void setRequest(HttpServletRequest req) {
        this.request = req;
    }

    public HttpServletRequest getRequest() {
        return request;
    }

    // 设置respond 对象
    public void setResponse(HttpServletResponse req) {
        this.response = req;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public void setInterceptors(List<AopMappingTag> aopMappingTags) {
        this.interceptors = aopMappingTags;
    }

    /**
     * 装载目标对象执行所需要的信息（类， 方法， 参数列表）<br/>
     * 每次请求，目标只有一个
     */
    // 需要执行的目标对象
    private MappingTag target;

    public void setTarget(MappingTag target) {
        this.target = target;
    }

    public ExecutorProxy(Configuration configuration) {
        this.configuration = configuration;
    }


    /**
     * 执行所有的切面和目标方法
     */
    public void execute() throws Exception {

        // 容器装载符合要求的拦截器 也就是说当前的请求 有那些拦截器进行拦截
        // 这里我们为何使用 LinkedList 的呢 因为LinkedList 有一个获取尾节点的功能 获取尾部的切面对象
        LinkedList<Interceptor> interceptorLinkedList = new LinkedList<>();
        //1、先执行切面前置方法 pre
        //遍历所有的切面，调用执行切面的前置方法prev
        for (AopMappingTag aopTag : interceptors) {
            // 拿到拦截器 Class信息
            Class aopClass = aopTag.getAopClass();
            // 实例化 切面对象
            Interceptor interceptor = (Interceptor) aopClass.getConstructor().newInstance();
            // 调用切面前置方法
            // 判断当前的拦截器是否执行成功
            try {
                if (interceptor.preHandle(request, response, target)) {
                    // 如果执行成功 将执行成功的拦截器放在集合中
                    // 执行成功的话需要后续继续执行 后置处理方法
                    interceptorLinkedList.add(interceptor);
                    //执行成功一个 跳过下面的代码 接着执行下一个循环
                    continue;
                } else {
                    //执行失败 抛出异常
                    //当前拦截器前置没有成功， 不在继续向下执行。
                    //  不再执行下一个切面或目标
                    //  不再执行当前拦截器的后置操作
                    //但之前执行过的拦截器，还要完成后置处理
                    //  interceptorList装载的拦截器，就是之前已经执行过的拦截器
                    //  只需要反向的执行post即可。
                    throw new Exception(aopClass + "拦截器执行失败");
                }
            } catch (Exception e) {
                // 如果前面执行拦截器的前置操作 操作出现异常 这里将会捕获
                //  但是在当前的拦截器抛出异常的 的前面的拦截器都是能正常执行的
                // 所以需要对正常执行完的拦截器 执行 后置方法
                // "\33[32m" 输出有颜色的字体
                System.out.println("\33[32m" + e.getMessage() + "\33[32m");
                // 执行之前能正常执行的拦截器的后置处理器
                while (interceptorLinkedList.size() > 0) {
                    // 从集合的后面进行获取 获取一个删除一个  因为拦截器做的是先进后出的方式
                    // 最先放入的拦截器的后置执行方法应该时候 最后出
                    Interceptor last = interceptorLinkedList.removeLast();
                    last.postHandle(request, response, target);
                }
                return;
            }
        }
        // 2、执行目标方法
        executeTarget();
        // 3、执行切面后置操作不
        //  如果前置操作正常执行完毕，那就会将执行过前置操作的切面对象装入list集合。
        //  此次就是反向执行post后置操作
        //  如果前置操作执行过程中失败，就会在前置处理中，对已经执行过来的拦截器，执行后置处理
        //  随着执行后置处理，就已经将list集合清空了
        //  所以此次的list.size=0也不会重复执行
        while (interceptorLinkedList.size() > 0) {
            // 获取最后一个并且移除
            Interceptor last = interceptorLinkedList.removeLast();
            last.postHandle(request, response, target);
        }


    }

    /**
     * 执行目标方法
     * 执行目标就是调用 controller.method 方法
     * 请求可能会有两种参数类型
     * 1、普通请求String
     * 2、文件请求  String+文件
     * 需要获取到方法的参数才能调用具体的方法
     */
    private void executeTarget() throws Exception {
        // 接收参数并且封装参数 所有的参数当前代理对象的map中
        receiveParemeter();

        // 处理完参数 需要进行参数的绑定
        // 所谓的绑定，就是根据参数列表，找到对应的参数数据
        Object[] value = bindParamDataForTargetMethod();
        // 得到方法参数值后
        // 获取对应执行的 controller 对象
        Object controller = configuration.getSingleObject(target.getMappingClass());
        // 获取目标方法
        Method method = target.getMethod();
        // 执行controller 对应的映射方法
        Object result = method.invoke(controller, value);

        // 根据执行目标方法的返回值 进行响应处理 转发？ 重定向？ 字符串？
        // 策略设计模式 进行处理
        List<ResponseHandleStrategy> handlers = configuration.getHandlers();
        for (ResponseHandleStrategy handler : handlers) {
            if (handler.isSupport(result, method)) {
                // 执行响应处理
                handler.handle(result, method, request, response);
                break;
            }
        }
    }

    /**
     * 需要进行参数的绑定
     * 为目标方法的参数列表，依次绑定数据 <br/>
     * 最终将绑定的数据组成一个Object[];
     * 所谓的绑定，就是根据参数列表，找到对应的参数数据
     * * 参数列表：每一个parameter对象
     * * 参数数据源： paramValues(strings) , fileParamValues(files) , servletValues(req,resp,session)
     * * 虽然目前有3种参数绑定形式
     * <p>
     * * 但未来框架内部还有可能会出现第4种，第5种，甚至是使用者希望自定义参数绑定。
     * 这里我们使用策略模式进行绑定器的使用
     *
     * @return
     */
    private Object[] bindParamDataForTargetMethod() throws Exception {
        // 创建一个对象 将当前请求的参数、文件参数、request、respond 保存
        // 参数数据源确实存在，但都是零散的    将所有的参数数据源综合管理   ParameterSource
        DefaultParameterSource parameterDataBind = new DefaultParameterSource();
        parameterDataBind.setParamValues(parameterMap);
        parameterDataBind.setFileParamValues(fileParamValues);
        parameterDataBind.setRequest(request);
        parameterDataBind.setResponse(response);

        // 执行目标方法
        // 获取目标方法对象 method
        Method method = target.getMethod();
        // 获取方法所有的参数对象
        Parameter[] parameters = method.getParameters();
        // 创建一个参数装载容器
        Object[] values = new Object[parameters.length];
        // 遍历参数
        for (int i = 0; i < values.length; i++) {
            // 获取当前参数对象
            Parameter parameter = parameters[i];
            //需要为当前的参数绑定 参数数据
            //需要一个参数绑定器
            //所有的参数绑定器都在configuration.bindersList集合中存储 在初始化的时候我们将保持好这些绑定器
            //需要依次遍历，找到能绑定当前参数的绑定器，使用其来绑定数据
            List<ParameterBindStrategy> parameterBindStrategyList = configuration.getBinders();
            for (ParameterBindStrategy binder : parameterBindStrategyList) {
                // 判断当前参数支持的绑定器类型
                if (binder.isSupport(parameter)) {
                    // 传入参数类型对象 parameter 参数数据源parameterDataBind
                    // 所谓的绑定就是 获取到方法中参数类型具体的
                    values[i] = binder.bind(parameter, parameterDataBind);
                    break;
                }
            }

        }
        return values;
    }

    /**
     * 接收参数并且封装参数 所有的参数当前代理对象的map中了
     *
     * @throws Exception
     */
    private void receiveParemeter() throws Exception {
        // 从request 获取参数 判断当前参数是否是文件参数
        // 需要借助 其他的工具jar包 做文件的处理 两个jar
        // 这里的request 参数在已经通过set方法传递过来
        //如果当前请求是文件类型
        if (ServletFileUpload.isMultipartContent(request)) {
            // 文件类型的包含两种参数在里面  文件流+普通的参数（String）
            // FileUpload 组件而言 会将两种参数类型都处理成FileItem对象
            // 获取一个工厂对象
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
            // 之前所有的参数都在request对象中
            // 现在请求的参数都封装在了FileItem对象中
            List<FileItem> fileItems = servletFileUpload.parseRequest(request);
            for (FileItem fileItem : fileItems) {
                // 每一个fileItem 对应着一个参数
                // 每个文件类型的fileItem 都会有两种类型 String  和 文件类型
                if (fileItem.isFormField()) {
                    // 说明是 普通的参数 String 类型的 <input name="uname" />
                    // 对这类参数 只需要获取参数名称和参数值即可
                    String fieldName = fileItem.getFieldName();// uname
                    String value = fileItem.getString("utf-8");
                    // 每次只能获取一个参数 但也可能存在多个同名的参数的情况
                    // 因为有多个String类型同名的参数  所以时候String[] 保存
                    // 判断当前的参数是否已经存在在Map集合中
                    String[] array = parameterMap.get(fieldName);
                    // 如果为空 说明是第一次添加
                    if (array == null) {
                        parameterMap.put(fieldName, new String[]{value});
                    } else {
                        // 不为空 说明之前已经添加过 所以对之前的参数进行追加
                        String[] newStrings = Arrays.copyOf(array, array.length + 1);
                        // 数组的方式的 就是直接创建一个数组 然后复制进去
                        newStrings[array.length - 1] = value;
                        parameterMap.put(fieldName, newStrings);
                    }

                } else {
                    // 说明是一个文件类型的参数 <input type=file name="photo" />
                    //文件信息包括：文件名，文件大小，文件类型，文件内容
                    // 获取文件名
                    String fieldName = fileItem.getName(); //photo
                    // 获取文件类型
                    String contentType = fileItem.getContentType();
                    // 获取文件大小
                    long size = fileItem.getSize();
                    // 获取文件内容 文件内容以字节数组形式存在
                    byte[] content = fileItem.get();
                    // 通过输入流间接获得文件内容
                    InputStream inputStream = fileItem.getInputStream();
                    //需要使用一个对象MvcFile，存储文件信息
                    MvcFile mvcFile = new MvcFile(fieldName, contentType, size, content, inputStream);
                    //接下来，需要将文件参数也缓存起来
                    //同时，文件参数也可能存在多个同名参数。
                    //所以最终也应该以数组的形式存储
                    // 到了这里就 也是判断当前的key 是否已经存在了
                    MvcFile[] mvcFiles = fileParamValues.get(fieldName);
                    if (mvcFiles == null) {
                        fileParamValues.put(fieldName, new MvcFile[]{mvcFile});
                    } else {
                        // 如果存在了 就需要将当前参数的值添加到数组中
                        mvcFiles = Arrays.copyOf(mvcFiles, mvcFiles.length + 1);
                        mvcFiles[mvcFiles.length - 1] = mvcFile;
                        fileParamValues.put(fieldName, mvcFiles);
                    }
                }
            }
        }
        // 到了这里 属于文件类型的参数已经全部处理完毕
        // 处理普通的参数
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String parameterName = parameterNames.nextElement();
            // 同一个参数可能存在多个同名参数 所以返回的是数组
            String[] parameterValues = request.getParameterValues(parameterName);
            parameterMap.put(parameterName, parameterValues);
        }
        //代码至此，就将不同情况下请求传递的参数都处理完毕了，paramValues，fileParamValues


    }


}
